/*
 * The MIT License
 * Copyright (c) 2011 kc4.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.kc4.util;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * リフレクションによるオブジェクト操作のためのユーティリティ。
 *
 * @author yoichi kikuchi
 * @since 1.0.0
 */
public class ReflectionUtils {

	private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionUtils.class);

	private static final String MESSAGE_DEFINE = "指定されたフィールドにアクセスできませんでした。フィールド名が正しく、アクセス可能なフィールド、またはアクセサが存在することを確認して下さい。class=%1$s, name=%2$s, value=%3$s.";

	private static final String REGEX_FIELD = "^([a-zA-Z_\\$]+[a-zA-Z_\\$0-9]*)(\\.([a-zA-Z_\\$]+[a-zA-Z_\\$0-9]*))*$";

	private static final String LITERAL_SEPARATOR = ".";

	private static final String LITERAL_EMPTY = "";

	private static final String FORMAT_OMIT = "\\Q%1$s.\\E";

	/** method cache */
	protected static final AccessorCache ACCESSOR = new AccessorCache();

	/** method cache */
	protected static final MethodCache CACHE = new MethodCache();

	/**
	 * リフレクションによりフィールドに対して与えるイベントです。
	 *
	 * @author yoichi kikuchi
	 * @since Magnolia Framework 2.0
	 */
	public static interface IEvent {

		/**
		 * フィールドに対して適用するイベントです。
		 *
		 * @param object 走査するオブジェクト。
		 * @param name イベントを適用するフィールドの名前。
		 * @return イベント処理後のオブジェクト。
		 */
		public Object onEvent(Object object, String name);
	}

	/**
	 * オブジェクトを走査し、すべてのフィールドに対してイベントを適用します。
	 * フィールドアクセスに関する例外は無視されます。
	 * これらは警告をマークしてロギングします。
	 * static フィールドは無視されます。
	 *
	 * @param object 走査するオブジェクト。
	 * @param event 適用するイベント。
	 */
	public static void scanAll(final Object object, final IEvent event) {

		scanAll(object, event, true);
	}

	/**
	 * オブジェクトを走査し、すべてのフィールドに対してイベントを適用します。
	 * static フィールドは無視されます。
	 *
	 * @param object 走査するオブジェクト。
	 * @param event 適用するイベント。
	 * @param ignoreException 例外を無視する場合は true、それ以外の場合は false。
	 */
	public static void scanAll(final Object object, final IEvent event, final boolean ignoreException) {

		scanAll(object, event, ignoreException, true);
	}

	/**
	 * オブジェクトを走査し、すべてのフィールドに対してイベントを適用します。
	 *
	 * @param object 走査するオブジェクト。
	 * @param event 適用するイベント。
	 * @param ignoreException 例外を無視する場合は true、それ以外の場合は false。
	 * @param ignoreStatic static フィールドを無視する場合は true、それ以外の場合は false。
	 */
	public static void scanAll(final Object object, final IEvent event, final boolean ignoreException, final boolean ignoreStatic) {

		Class<?> clazz = object.getClass();

		do {

			for (Field f : clazz.getDeclaredFields()) {

				if (ignoreStatic && !Modifier.isStatic(f.getModifiers())) {

					scan(object, f.getName(), event, ignoreException);
				}
			}

		}
		while ((clazz = clazz.getSuperclass()) != null);
	}

	/**
	 * オブジェクトを走査し、指定された名前のフィールドに対してイベントを適用します。
	 * フィールド内のメンバーを再帰的にたどって走査する場合には、フィールド名を "." で区切って表現します。
	 * 例えば、<i>Office</i> というクラスに <i>List&lt;Employee&gt; emp</i> というメンバーがあり、
	 * <i>Employee</i> クラス内の <i>name</i> フィールドに対してイベントを適用したい場合には
	 * <i>Office</i> インスタンスに対して、"emp.name" という名前を指定します。
	 * フィールドアクセスに関する例外は無視されます。
	 * これらは警告をマークしてロギングします。
	 *
	 * @param object 走査するオブジェクト。
	 * @param name 処理するフィールドの名前。
	 * @param event 適用するイベント。
	 */
	public static void scan(final Object object, final String name, final IEvent event) {

		scan(object, name, event, true);
	}

	/**
	 * オブジェクトを走査し、指定された名前のフィールドに対してイベントを適用します。
	 * フィールド内のメンバーを再帰的にたどって走査する場合には、フィールド名を "." で区切って表現します。
	 * 例えば、<i>Office</i> というクラスに <i>List&lt;Employee&gt; emp</i> というメンバーがあり、
	 * <i>Employee</i> クラス内の <i>name</i> フィールドに対してイベントを適用したい場合には
	 * <i>Office</i> インスタンスに対して、"emp.name" という名前を指定します。
	 *
	 * @param object 走査するオブジェクト。
	 * @param name 処理するフィールドの名前。
	 * @param event 適用するイベント。
	 * @param ignoreException 例外を無視する場合は true、それ以外の場合は false。
	 */
	public static void scan(final Object object, final String name, final IEvent event, final boolean ignoreException) {

		if (object == null || name == null || event == null) {

			return;
		}

		if (name.matches(REGEX_FIELD)) {

			int index = name.indexOf(LITERAL_SEPARATOR);
			boolean isRequireRecursion = index >= 0;
			String sub = null;
			String omit = null;

			if (isRequireRecursion) {

				/*
				 * 再帰によるフィールド走査が必要
				 */

				sub = name.substring(0, index);
				omit = name.replaceFirst(String.format(FORMAT_OMIT, sub), LITERAL_EMPTY);

				if (object.getClass().isArray()) {

					// 配列の場合、拡張 for にて内部走査と再帰
					for (Object o : (Object[]) object) {

						scan(get(o, sub, null, ignoreException), omit, event, ignoreException);
					}
				}
				else if (object instanceof Iterable<?>) {

					// Iterable インスタンスの場合、拡張 for にて内部走査と再帰
					for (Object o : (Iterable<?>) object) {

						if (!omit.equals(LITERAL_EMPTY)) {

							scan(get(o, sub, null, ignoreException), omit, event, ignoreException);
						}
					}
				}
				else {

					// その他のオブジェクトの場合、再帰
					scan(get(object, sub, null, ignoreException), omit, event, ignoreException);
				}
			}
			else {

				/*
				 * 再帰不要時は name からフィールドを検索し
				 * 見つかった値を検証
				 */

				Object getValue = get(object, name, null, ignoreException);

				Object setValue = event.onEvent(getValue, name);

				// イベントの結果、フィールドの値に変更があった場合のみ更新
				if (getValue != setValue) {

					set(object, name, setValue, ignoreException);
				}
			}
		}
	}

	/**
	 * フィールドの値をアクセサを経由して取得します。
	 * フィールドアクセスに関する例外は無視されます。
	 * これらは警告をマークしてロギングします。
	 *
	 * @param object 取得する値を保持するフィールドを持つオブジェクト。
	 * @param name フィールド名。
	 * @return フィールドから取得した値。
	 */
	public static Object get(final Object object, final String name) {

		return get(object, name, null);
	}

	/**
	 * フィールドの値をアクセサを経由して取得します。
	 * 取得した値が null の場合は、defaultValue に指定した値が返却されます。
	 * 値取得時に例外が発生した場合は defaultValue の設定にかかわらず、null が返却されます。
	 * フィールドアクセスに関する例外は無視されます。
	 * これらは警告をマークしてロギングします。
	 *
	 * @param object 取得する値を保持するフィールドを持つオブジェクト。
	 * @param name フィールド名。
	 * @param defaultValue 既定の値。
	 * @return フィールドから取得した値。
	 */
	public static Object get(final Object object, final String name, final Object defaultValue) {

		return get(object, name, defaultValue, true);
	}

	/**
	 * フィールドの値をアクセサを経由して取得します。
	 * 取得した値が null の場合は、defaultValue に指定した値が返却されます。
	 * 値取得時に例外が発生し、ignoreException に true が設定されていた場合は defaultValue の設定にかかわらず、null が返却されます。
	 *
	 * @param object 取得する値を保持するフィールドを持つオブジェクト。
	 * @param name フィールド名。
	 * @param defaultValue 既定の値。
	 * @param ignoreException 例外を無視する場合は true、それ以外の場合は false。
	 * @return フィールドから取得した値。
	 */
	public static Object get(final Object object, final String name, final Object defaultValue, final boolean ignoreException) {

		try {

			Object o = ACCESSOR.get(object.getClass(), name).invoke(object);

			return o != null ? o : defaultValue;
		}
		catch (RuntimeException e) {

			if (ignoreException) {

				if (LOGGER.isDebugEnabled()) {

					LOGGER.warn(String.format(MESSAGE_DEFINE, object.getClass().getName(), name, null), e);
				}
			}
			else {

				throw e;
			}
		}
		catch (Exception e) {

			if (ignoreException) {

				if (LOGGER.isDebugEnabled()) {

					LOGGER.warn(String.format(MESSAGE_DEFINE, object.getClass().getName(), name, null), e);
				}
			}
			else {

				throw new RuntimeException(e);
			}
		}

		return null;
	}

	/**
	 * フィールドの値をアクセサを経由して設定します。
	 * フィールドアクセスに関する例外は無視されます。
	 * これらは警告をマークしてロギングします。
	 *
	 * @param object 設定する値を保持させるフィールドを持つオブジェクト。
	 * @param name フィールド名。
	 * @param value フィールドに設定する値。
	 */
	public static void set(final Object object, final String name, final Object value) {

		set(object, name, value, true);
	}

	/**
	 * フィールドの値をアクセサを経由して設定します。
	 *
	 * @param object 設定する値を保持させるフィールドを持つオブジェクト。
	 * @param name フィールド名。
	 * @param value フィールドに設定する値。
	 * @param ignoreException 例外を無視する場合は true、それ以外の場合は false。
	 */
	public static void set(final Object object, final String name, final Object value, final boolean ignoreException) {

		try {

			ACCESSOR.get(object.getClass(), name, value.getClass()).invoke(object, value);
		}
		catch (RuntimeException e) {

			if (ignoreException) {

				if (LOGGER.isDebugEnabled()) {

					LOGGER.warn(String.format(MESSAGE_DEFINE, object.getClass().getName(), name, value), e);
				}
			}
			else {

				throw e;
			}
		}
		catch (Exception e) {

			if (ignoreException) {

				if (LOGGER.isDebugEnabled()) {

					LOGGER.warn(String.format(MESSAGE_DEFINE, object.getClass().getName(), name, value), e);
				}
			}
			else {

				throw new RuntimeException(e);
			}
		}
	}

	/**
	 * 任意のクラスから静的な valueOf メソッドを呼び出します。
	 * 変換に失敗した場合は、例外がエラーとしてロギングされ、null が返却されます。
	 *
	 * @param <T> 変換タイプ。
	 * @param o 変換する文字列。
	 * @param clazz 静的な velueOf メソッドの定義されている変換タイプ。
	 * @return 変換された値。
	 */
	@SuppressWarnings("unchecked")
	public static <T> T valueOf(final Class<T> clazz, final String o) {

		try {

			return (T) CACHE.get(clazz, "valueOf", String.class).invoke(null, o);
		}
		catch (Exception e) {

			LOGGER.error(e.getMessage(), e);
		}

		return null;
	}

	/**
	 * オブジェクトから任意のインスタンスメソッドを呼び出します。
	 * メソッドの呼び出し、実行に失敗した場合は、例外がエラーとしてロギングされ、null が返却されます。
	 *
	 * @param <T> 戻り値タイプ。
	 * @param o メソッドを呼び出すオブジェクト。
	 * @param name メソッド名。
	 * @param args 引数。
	 * @return メソッドの戻り値。
	 */
	@SuppressWarnings("unchecked")
	public static <T> T invoke(final Object o, final String name, final Object... args) {

		Class<?>[] clazzes = new Class<?>[args.length];

		for (int i = 0; i < args.length; i++) {

			clazzes[i] = args[i].getClass();
		}

		try {
			return (T) CACHE.get(o.getClass(), name, clazzes).invoke(o, args);
		}
		catch (Exception e) {

			LOGGER.error(e.getMessage(), e);
		}

		return null;
	}
}
